/////////////// LEXER

function Token(type, repr, val) {

  this.getType = function() {
    return type;
  };
  
  this.getRepr = function() {
    return repr;
  };
  
  this.getVal = function() {
    return val;
  };
  
  this.toString = function() {
    return "Token<" + type + "," + repr + "," + val + ">";
  };
  
}

function LexingException(msg) {
  this.toString = function() {
    return msg;
  };
}

function Lexer(input) {

  var scannedTokens = [];
  var currentTokenIndex = 0;
  var pos = 0;
  
  this.getInput = function() {
    return input;
  };

  this.getCurrent = function() {
    if (currentTokenIndex >= scannedTokens.length) {
      scannedTokens.push(getNext());
    }

    return scannedTokens[currentTokenIndex];
  };

  // Support for backtracking
  this.movePrev = function() {
    currentTokenIndex -= 1;
  };

  this.moveNext = function() {
    currentTokenIndex += 1;
  };
  
  // Character recognizing predicates
  
  function isNameBegin(c) {
    return (
      Extras.range('A', 'Z').contains(c) ||
      Extras.range('a', 'z').contains(c) ||
      ['+', '-', '.', '<', '>', '*', '/', '#'].contains(c));
  }
  
  function isNameContinue(c) {
    return (
      isNameBegin(c) ||
      isNumeric(c) ||
      ['='].contains(c));
  }
  
  function isNumberBegin(c) {
    return (
      isNumeric(c));
  }
  
  function isNumberContinue(c) {
    return (
      isNumeric(c) ||
      ['.'].contains(c));
  }
  
  function isNumeric(c) {
    return Extras.range('0', '9').contains(c);
  }
  
  function isWhitespace(c) {
    return c === ' ';
  }

  function getNext() {

    // More data in input buffer
    if (pos < input.length) {
    
      var c = input.charAt(pos);
      
      if (c === '-') {
        if (isNumberBegin(input.charAt(pos + 1))) {
          return getNumber();
        }
        else {
          return getName();
        }
      }
      
      else if (isNameBegin(c)) {
        return getName();
      }
      
      else if (isNumberBegin(c)) {
        return getNumber();
      }
      
      else if (isWhitespace(c)) {
        pos += 1;
        return getNext();
      }
      
      else {
        switch (c) {
          case ('['):
            return getOperator(new Token('openBlock', '['));
          case (']'):
            return getOperator(new Token('closeBlock', ']'));
          case ('='):
            return getOperator(new Token('equals', '='));
          case (';'):
            return getOperator(new Token('semiColon', ';'));
          default:
            throw new LexingException("unrecognized character: <" + c + ">");
        }
        return getName();
      }
    }
    
    // Input buffer is empty
    else {
      return null;
    }

  }

  function getNumber() {
    var output = "";

    if (input.charAt(pos) === '-') {
      output += "-";
      pos += 1;
    }
    
    while (pos < input.length && isNumberContinue(input.charAt(pos))) {
      output += input.charAt(pos);
      pos += 1;
    }

    return new Token('number', output, parseFloat(output));
  }
  
  function getName() {
    var output = "";

    while (pos < input.length && isNameContinue(input.charAt(pos))) {
      output += input.charAt(pos);
      pos += 1;
    }

    return new Token('name', output);
  }

  function getOperator(token) {
    pos += 1;
    return token;
  }

}
